-- Global object loader
function objectLoader(object_name, ...)
    if object_name == "door" then        
        return Door.new(arg[1], arg[2], arg[3], arg[4])
    elseif object_name == "tutorial npcs" then
        return TutorialNPCs.new()
    elseif object_name == "birds" then
        return Birds.new(0, 0, 1280, 320)
    elseif object_name == "boat" then
        return Boat.new()
    elseif object_name == "block" then
        return Blocks.new(arg[1], arg[2], arg[3], arg[4], arg[5])
    end
end

-- World Objects
-- index
-- [BLO] Block
-- [BIR] Village Birds
-- [TUT] Tutorial NPCS
-- [SHP] Boat

PlayerSpawn = {}
PlayerSpawn.__index = PlayerSpawn

function PlayerSpawn.new(x, y)
    local p = {}
    setmetatable(p, PlayerSpawn)
    p.x = x
    p.y = y
    return p
end

-- Doors [DOO]
Door = {}
Door.__index = Door

function Door.new(x, y, connected_ids, door_type)
    local d = {}
    setmetatable(d, Door)
    
    -- variables
    d.c_type = "door"
    d.initial_x = x
    d.initial_y = y
    d.x = x
    d.y = y
    d.sprite = Sprite.new()
    d.sprite:loadImage("door", "content/world/block.png")
    d.bounding_box = Rectangle.new(d.x, d.y, 64, 64)
    d.state = "closed" -- closed -> opening -> opened
    d.elapsed_time = 0.0
    d.quake_effect = QuakeEffect.new()
    d.connected_ids = connected_ids
    d.door_type = door_type

    return d
end

-- init
function Door:init(prev_id)
    -- if prev id is same as in connected_ids array
    -- then do not init
    if self.connected_ids ~= nil then
        for y = 1, #self.connected_ids do
            if prev_id == self.connected_ids[y] then
                return
            end
        end
    end

    self.y = self.initial_y
    self.x = self.initial_x
    self.state = "closed"
    self.elapsed_time = 0.0
end

-- draw
function Door:draw(camera)
    -- replace this with door graphics
    for y = 0, 1 do
        for x = 0, 1 do
            self.sprite:draw(camera:getRenderX(self.x + (x * 32)), 
                camera:getRenderY(self.y + (y * 32)))
        end
    end
end

-- effects
function Door:drawEffects(camera)
end

-- activates door
function Door:activateDoor()
    if self.state == "closed" then
        self.state = "opening"
        self.quake_effect:play(0.5, 0.05, 2)
    end
end

-- update
function Door:update()
    if self.state == "opening" then
        self.quake_effect:update()
        self.elapsed_time = self.elapsed_time + global_delta_time
        
        -- apply quake effect also
        self.y = self.initial_y - (math.pow(self.elapsed_time, 2) * 64) + self.quake_effect.y
        self.x = self.initial_x + self.quake_effect.x
        
        if self.y <= self.initial_y - 64 then
            self.state = "opened"
            self.quake_effect:stop()
        end
    end
end

-- check wheter player should take action to collision detection
function Door:shouldApplyResponse()
    if self.y > self.initial_y - 32 then
        return true
    else
        return false
    end
end

-- collided
function Door:isCollided(obj)
    if intersect(self.bounding_box, obj.bounding_box) then
        return true
    else
        return false
    end
end

-- toString
function Door:toString()
    return "Door x: " .. self.x .. " y: " .. self.y .. " type: " .. self.door_type
end

-- Breakable blocks [BLO]
Blocks = {}
Blocks.__index = Blocks

function Blocks.new(x, y, width, height, b_type)
    local b = {}
    setmetatable(b, Blocks)
    
    -- constans
    b.SIZE = 32
    b.NORMAL = 2
    b.FIRE = 3
    b.ICE = 4
    b.EARTH = 5
    
    -- variables
    b.c_type = "block"
    b.x = x
    b.y = y
    b.w = width
    b.h = height
    b.sprite = Sprite.new()
    b.bounding_box = Rectangle.new(x, y, b.w, b.h)
    b.state = "alive"
    b.type = b_type
    
    -- init specific tile
    if b.type == "normal" then
        b.sprite:loadImage("block", "content/world/block.png")
        b.max_hp = b.NORMAL
    end
    
    -- current hp
    b.hp = b.max_hp
    
    return b
end

-- init
function Blocks:init(prev_id)
    -- block doors are init only once when map is loaded.
end

-- draw
function Blocks:draw(camera)
    if self.state == "dead" then return end

    local y_max = math.floor(self.h / self.SIZE) - 1
    local x_max = math.floor(self.w / self.SIZE) - 1
    
    for y = 0, y_max do
        for x = 0, x_max do
            self.sprite:draw(camera:getRenderX(self.x + (x * self.SIZE)), 
                camera:getRenderY(self.y + (y * self.SIZE)))
        end
    end
    
    --self.sprite:draw(camera:getRenderX(self.x), camera:getRenderY(self.y))
    --self.sprite:draw(camera:getRenderX(self.x), camera:getRenderY(self.y + 32))
end

-- effects
function Blocks:drawEffects(camera)
end

-- update
function Blocks:update()
    if self.state == "dead" then return end
    
    -- update sprite / flickering mostly
    self.sprite:update()
end

-- take hit
function Blocks:takeHit()
    self.hp = self.hp - 1
    self.sprite:playFlickering(0.2, 0.2)
    
    if self.hp <= 0 then
        self.state = "dead"
    end
end

-- can take hit
function Blocks:canTakeHit()
    return (not self.sprite.is_hit)
end

-- collide with player / object
function Blocks:isCollided(obj)
    if intersect(obj.bounding_box, self.bounding_box) then
        return true
    end
    
    return false
end

-- Birds [BIR]
Birds = {}
Birds.__index = Birds

function Birds.new(min_x, min_y, max_x, max_y)
    local b = {}
    setmetatable(b, Birds)
    
    -- consts
    b.c_type = "birds"
    b.MAX_ALTITUDE = 50
    b.SPEED = 1.0
    b.SIN_SPEED = 0.25
    
    -- variables
    -- Lentavat linnut
    b.min_x = min_x
    b.max_x = max_x
    b.min_y = min_y
    b.max_y = max_y
    b.flying = Sprite.new()
    b.flying:loadImage("bird", "content/world/bird.png")
    b.flying:addAnimation("bird-anim", "bird", 22, 26, {0, 1}, 0.12, true)
    b.flying_x = 0
    b.flying_y = 0
    b.flying_initial_x = 0
    b.flying_initial_y = 0
    b.flying_time = 0.0
    b.flying_difference_x = 0
    
    math.randomseed(os.time())
    
    b.flying_difference_x = math.random(50, 150)
    b.flying_initial_y = math.random(b.MAX_ALTITUDE, b.MAX_ALTITUDE * 2)
    b.flying_x = math.random(-250, -150)
    
    -- Paikallaan olevat, jotka lahtevat karkuun
    b.sitting = Sprite.new()
    b.sitting:loadImage("bird", "content/world/bird.png")
    b.sitting:addAnimation("flying", "bird", 22, 26, {0, 1}, 0.12, true)
    b.sitting:addAnimation("sitting", "bird", 22, 26, {4}, 0.12, false)
    b.sitting:playAnimation("sitting")
    b.sitting:flip(true, false)
    b.sitting:update()
    
    b.sitting_x = math.random(b.min_x + 150, b.max_x - 150)
    b.sitting_y = 256 - 26
    b.sitting_state = "idle"
    
    return b
end

function Birds:init(prev_id)
end

function Birds:updateFlyingBirds()
    if self.sitting_state ~= "out" then return end

    self.flying:update()
    self.flying_time = (self.flying_time + global_delta_time) % ((math.pi * 2) / self.SIN_SPEED)
    self.flying_y = self.flying_initial_y + math.sin(self.flying_time * self.SIN_SPEED) * self.MAX_ALTITUDE
    self.flying_x = self.flying_x + self.SPEED
    
    if self.flying_x > self.max_x then
        self.flying_initial_x = math.random(150, 250)
        self.flying_x = -self.flying_initial_x
        self.flying_intial_y = math.random(self.MAX_ALTITUDE, self.MAX_ALTITUDE * 2)
        self.flying_y = self.flying_initial_y
        self.flying_difference_x = math.random(50, 150)
    end
end

function Birds:updateSittingBirds()
    if self.sitting_state == "out" then return end

    self.sitting:update()
    
    if self.sitting_state == "fly" then
        self.sitting_y = self.sitting_y - 2
        self.sitting_x = self.sitting_x - 2
        
        if self.sitting_y < 0 then
            self.sitting_state = "out"
        end
    end
end

function Birds:activateBirds(player)
    if self.sitting_state == "out" then return end

    local x_distance = math.abs(player.x - self.sitting_x)
    local x_distance_2 = math.abs(player.x - (self.sitting_x + 50))
    if x_distance < 25 or x_distance_2 < 25 then
        self.sitting_state = "fly"
        self.sitting:playAnimation("flying")
    end
end

function Birds:update()
    self:updateFlyingBirds()
    self:updateSittingBirds()
end

function Birds:draw(camera)
    -- Piirretaan kolme lintua lenteleen
    if self.sitting_state == "out" then
        self.flying:draw(camera:getRenderX(self.flying_x), camera:getRenderY(self.flying_y))
        self.flying:draw(camera:getRenderX(self.flying_x + self.flying_difference_x), camera:getRenderY(self.flying_y + 10))
        self.flying:draw(camera:getRenderX(self.flying_x + self.flying_difference_x - 20), camera:getRenderY(self.flying_y - 30))
    end
    
    -- Piirretaan paikallaan olevat linnut, kolme myoskin
    if self.sitting_state == "idle" then
        self.sitting:draw(camera:getRenderX(self.sitting_x), camera:getRenderY(self.sitting_y))
        self.sitting:draw(camera:getRenderX(self.sitting_x + 20), camera:getRenderY(self.sitting_y))
        self.sitting:draw(camera:getRenderX(self.sitting_x + 50), camera:getRenderY(self.sitting_y))
    elseif self.sitting_state == "fly" then
        self.sitting:draw(camera:getRenderX(self.sitting_x), camera:getRenderY(self.sitting_y - 4))
        self.sitting:draw(camera:getRenderX(self.sitting_x + 20), camera:getRenderY(self.sitting_y))
        self.sitting:draw(camera:getRenderX(self.sitting_x + 50), camera:getRenderY(self.sitting_y - 2))
    end
end

-- effects
function Birds:drawEffects(camera)
end

-- Tutorial NPCS [TUT]
TutorialNPCs = {}
TutorialNPCs.__index = TutorialNPCs

function TutorialNPCs.new()
    local s = {}
    setmetatable(s, TutorialNPCs)
    
    -- variables
    s.c_type = "tutorial npcs"
    s.c_speech_bubble_speed = 0.5
    s.sprite = Sprite.new()
    s.sprite:loadImage("old-man", "content/world/old-man.png")
    s.sprite:loadImage("old-woman-0", "content/world/old-woman-0.png")
    s.sprite:loadImage("old-woman-1", "content/world/old-woman-1.png")
    s.sprite:loadImage("young-girl", "content/world/young-girl.png")
    
    s.talkers = {"old-man",
                 "old-woman-0",
                 "old-woman-1",
                 "young-girl"}
    
    -- Sama jarjestys kuin edella.
    s.hitboxes = {Rectangle.new(608, 256 - 40, 40, 40),
                  Rectangle.new(512, 256 - 40, 40, 40),
                  Rectangle.new(928, 256 - 40, 40, 40),
                  Rectangle.new(288, 256 - 40, 40, 40)}
                  
    s.show_speech_bubble = false
    s.npcs_index = 0
    s.speech_bubble = Sprite.new()
    s.speech_bubble:loadImage("bubble", "content/gui/speech-bubble.png")
    s.speech_bubble:setScale(0.0, 0.0)
    s.speech_bubble:setOrigin(11, 6)
    s.elapsed_time = 0.0
    s.scale = 0.0
    
    return s
end

-- Init
function TutorialNPCs:init(prev_id)
    -- do nothing
end

-- Draws the speech bubble and the npcs
function TutorialNPCs:draw(camera)
    -- flip
    self.sprite:flip(true, false)
    
    -- draw characters
    self.sprite:setImage("old-man")
    self.sprite:draw(camera:getRenderX(608), camera:getRenderY(256 - self.sprite:getHeight()))
    
    self.sprite:setImage("old-woman-0")
    self.sprite:flip(false, false)
    self.sprite:draw(camera:getRenderX(512), camera:getRenderY(256 - self.sprite:getHeight()))
    self.sprite:flip(true, false)
    
    self.sprite:setImage("old-woman-1")
    self.sprite:draw(camera:getRenderX(928), camera:getRenderY(256 - self.sprite:getHeight()))
    
    self.sprite:setImage("young-girl")
    self.sprite:draw(camera:getRenderX(288), camera:getRenderY(256 - self.sprite:getHeight()))
    
    if self.show_speech_bubble then
        local rect = self.hitboxes[self.npcs_index]
        self.speech_bubble:draw(camera:getRenderX(rect.x + 20), camera:getRenderY(rect.y - 10))
    end
end

-- effects
function TutorialNPCs:drawEffects(camera)
end

-- Updates the speech bubble animation
function TutorialNPCs:update()
    if self.show_speech_bubble then
        if self.scale < 1.0 then
            self.elapsed_time = self.elapsed_time + global_delta_time
        end
        
        -- modify scale
        self.scale = math.pow(self.elapsed_time * 3, 2) * 1.0
        self.speech_bubble:setScale(self.scale, self.scale)
    end
end

-- Check collisions against with the given object
function TutorialNPCs:isCollided(obj)
    local obj_b = obj.bounding_box
    for i = 1, #self.hitboxes do
        if intersect(obj_b, self.hitboxes[i]) then
            self.show_speech_bubble = true
            self.npcs_index = i
            return true
        end
    end
    
    -- disable speech bubble not collided.
    if self.show_speech_bubble then
        self.show_speech_bubble = false
        self.elapsed_time = 0
        self.scale = 0
        self.speech_bubble:setScale(0.0, 0.0)
        self.npcs_index = 0
    end
    
    return false
end

-- Activates current dialog
function TutorialNPCs:activateDialog()
    if self.npcs_index ~= 0 then
        self.show_speech_bubble = false
        screen_manager:pushScreen(DialogPopupScreen.new(self.talkers[self.npcs_index]))
        return true
    end
    
    return false
end

-- Ship, from village [SHP]
Boat = {}
Boat.__index = Boat

function Boat.new()
    local s = {}
    setmetatable(s, Boat)
    
    -- variables
    s.c_type ="boat"
    s.sail = Sprite.new()
    s.sail:loadImage("sail", "content/world/sail.png")
    s.sail:addAnimation("sail-anim", "sail", 112, 140, {0, 1, 2, 3}, 0.25, true)
    
    s.boat = Sprite.new()
    s.boat:loadImage("boat", "content/world/boat.png")
    
    s.dock = Sprite.new()
    s.dock:loadImage("dock", "content/world/dock.png")
    
    s.water = Sprite.new()
    s.water:loadImage("water", "content/world/water.png")
    
    s.y_offset = 0
    s.elapsed_time = 0.0
    
    return s
end

function Boat:init(prev_id)
    -- nothing
end

function Boat:draw(camera)
    self.sail:draw(camera:getRenderX(0), camera:getRenderY(106 + self.y_offset))
    self.boat:draw(camera:getRenderX(-82), camera:getRenderY(100 + self.y_offset))
    self.dock:draw(camera:getRenderX(0), camera:getRenderY(256))
    self.water:draw(camera:getRenderX(0), camera:getRenderY(268))
end

-- effects
function Boat:drawEffects(camera)
end

function Boat:update()
    self.sail:update()
    self.y_offset = math.sin(self.elapsed_time * 2) * 3
    self.elapsed_time = (self.elapsed_time + global_delta_time) % (2 * math.pi)
end